﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using DaveSexton.Labs.Properties;

namespace DaveSexton.Labs
{
	public sealed class LabCategory
	{
		#region Public Properties
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes",
			Justification = "Object is immutable.")]
		public static readonly LabCategory Empty = new LabCategory();

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1820:TestForEmptyStringsUsingStringLength",
			Justification = "Contract is stating that the result will be equal to string.Empty.")]
		public string Name
		{
			get
			{
#if !WINDOWS_PHONE_70
				Contract.Ensures(this == Empty || !string.IsNullOrWhiteSpace(Contract.Result<string>()));
#else
				Contract.Ensures(this == Empty || !string.IsNullOrEmpty(Contract.Result<string>()));
#endif
				Contract.Ensures(this != Empty || Contract.Result<string>() == string.Empty);

				Contract.Assume((name == null) == (this == Empty));
#if !WINDOWS_PHONE_70
				Contract.Assert(this == Empty || !string.IsNullOrWhiteSpace(name));
#else
				Contract.Assert(this == Empty || !string.IsNullOrEmpty(name));
#endif

				return name ?? string.Empty;
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1820:TestForEmptyStringsUsingStringLength",
			Justification = "Contract is stating that the result will be equal to string.Empty.")]
		public string OrganizedName
		{
			get
			{
#if !WINDOWS_PHONE_70
				Contract.Ensures(IsRoot || this == Empty || !string.IsNullOrWhiteSpace(Contract.Result<string>()));
#else
				Contract.Ensures(IsRoot || this == Empty || !string.IsNullOrEmpty(Contract.Result<string>()));
#endif
				Contract.Ensures(!IsRoot || this != Empty || Contract.Result<string>() == string.Empty);

				if (organizedName == null)
				{
					if (IsRoot || this == Empty)
					{
						organizedName = string.Empty;
					}
					else
					{
						organizedName = name;

						LabCategory current = parent;

						while (!current.IsRoot)
						{
							organizedName = string.Format(CultureInfo.CurrentCulture, Resources.CategoryNameFormat, current.Name, organizedName);

							current = current.Parent;
						}
					}
				}

#if !WINDOWS_PHONE_70
				Contract.Assume(IsRoot || this == Empty || !string.IsNullOrWhiteSpace(organizedName));
#else
				Contract.Assume(IsRoot || this == Empty || !string.IsNullOrEmpty(organizedName));
#endif
				Contract.Assume(!IsRoot || this != Empty || organizedName == string.Empty);

				return organizedName;
			}
		}

		public IList<Lab> Labs
		{
			get
			{
				Contract.Ensures(Contract.Result<IList<Lab>>() != null);
				Contract.Ensures(Contract.Result<IList<Lab>>().IsReadOnly);
				////Contract.Ensures(Contract.ForAll(labs, lab => lab != null));

				return labs;
			}
		}

		public IEnumerable<Lab> LabsRecursive
		{
			get
			{
				Contract.Ensures(Contract.Result<IEnumerable<Lab>>() != null);

				foreach (var category in CategoriesRecursive)
				{
					Contract.Assume(category.labs != null);

					foreach (var lab in category.labs)
					{
						yield return lab;
					}
				}
			}
		}

		public LabCategory Parent
		{
			get
			{
				Contract.Ensures(Contract.Result<LabCategory>() != null);
				Contract.Ensures((Contract.Result<LabCategory>() != Empty) == (this != Empty && !IsRoot));

				Contract.Assume((parent != Empty) == (this != Empty && !IsRoot));

				return parent;
			}
		}

		public IList<LabCategory> Subcategories
		{
			get
			{
				Contract.Ensures(Contract.Result<IList<LabCategory>>() != null);
				Contract.Ensures(Contract.Result<IList<LabCategory>>().IsReadOnly);
				////Contract.Ensures(Contract.ForAll(subcategories, category => category != null));
				////Contract.Ensures(Contract.ForAll(subcategories, category => category.IsInitialized));

				// reported CC bug: http://social.msdn.microsoft.com/Forums/en-US/codecontracts/thread/4b06622c-e2e6-4d94-bf2c-e7724909c3c6/
				return subcategories;
			}
		}

		/// <summary>
		/// Gets an enumerable that yields this category and then iterates over all of its <see cref="Subcategories"/>, recursively.
		/// </summary>
		public IEnumerable<LabCategory> CategoriesRecursive
		{
			get
			{
				Contract.Ensures(Contract.Result<IEnumerable<LabCategory>>() != null);

				return CategoriesRecursiveIterator();
			}
		}

		public int Depth
		{
			get
			{
				Contract.Ensures(!IsRoot || Contract.Result<int>() == 0);
				Contract.Ensures(IsRoot || Contract.Result<int>() > 0);

				return depth;
			}
		}

		public bool IsRoot
		{
			get
			{
				Contract.Ensures(Contract.Result<bool>() == (depth == 0));

				return depth == 0;
			}
		}
		#endregion

		#region Private / Protected
		private static string DefaultCategoryName
		{
			get
			{
#if !WINDOWS_PHONE_70
				Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result<string>()));
#else
				Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()));
#endif

				var name = Resources.DefaultCategoryName;

#if !WINDOWS_PHONE_70
				Contract.Assume(!string.IsNullOrWhiteSpace(name));
#else
				Contract.Assume(!string.IsNullOrEmpty(name));
#endif

				return name;
			}
		}

		private readonly string name;
		private readonly IList<Lab> labs;
		private readonly IList<LabCategory> subcategories;
		[ContractPublicPropertyName("Depth")]
		private readonly int depth;
		private string organizedName;
		private LabCategory parent;
		#endregion

		#region Constructors
		private LabCategory()
		{
			parent = this;

			labs = new List<Lab>(0).AsReadOnly();
			subcategories = new List<LabCategory>(0).AsReadOnly();

			Contract.Assume(labs.IsReadOnly);
			Contract.Assume(subcategories.IsReadOnly);
		}

		private LabCategory(string name, int depth, IEnumerable<Lab> labs, IEnumerable<LabCategory> subcategories)
		{
#if !WINDOWS_PHONE_70
			Contract.Requires(!string.IsNullOrWhiteSpace(name));
#else
			Contract.Requires(!string.IsNullOrEmpty(name));
#endif
			Contract.Requires(depth > 0);
			Contract.Requires(labs != null);
			////Contract.Requires(Contract.ForAll(labs, lab => lab != null));
			Contract.Requires(subcategories != null);
			////Contract.Requires(Contract.ForAll(subcategories, category => category != Empty));
			Contract.Ensures(this.depth > 0);

			this.name = name;
			this.depth = depth;
			this.labs = labs.ToList().AsReadOnly();
			this.subcategories = subcategories.ToList().AsReadOnly();

			Contract.Assume(this.labs.IsReadOnly);
			Contract.Assume(this.subcategories.IsReadOnly);

			parent = Empty;		// This is just to make the static checker happy.  It will be assigned before the Organize method completes.

			foreach (var lab in labs)
			{
				Contract.Assume(lab != null);

				lab.Category = this;
			}

			foreach (var category in subcategories)
			{
				Contract.Assume(category != null);

				category.parent = this;
			}

			Contract.Assume(this != Empty);
		}

		private LabCategory(IEnumerable<Lab> labs, IEnumerable<LabCategory> subcategories)
		{
			Contract.Requires(labs != null);
			////Contract.Requires(Contract.ForAll(labs, lab => lab != null));
			Contract.Requires(subcategories != null);
			////Contract.Requires(Contract.ForAll(subcategories, category => category != Empty));
			Contract.Ensures(depth == 0);

			this.name = DefaultCategoryName;
			this.labs = labs.ToList().AsReadOnly();
			this.subcategories = subcategories.ToList().AsReadOnly();

			Contract.Assume(this.labs.IsReadOnly);
			Contract.Assume(this.subcategories.IsReadOnly);

			parent = Empty;

			foreach (var lab in labs)
			{
				Contract.Assume(lab != null);

				lab.Category = this;
			}

			foreach (var category in subcategories)
			{
				Contract.Assume(category != null);

				category.parent = this;
			}

			Contract.Assume(this != Empty);
		}
		#endregion

		#region Methods
		[ContractInvariantMethod]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
		private void ObjectInvariant()
		{
			Contract.Invariant(parent != null);
			Contract.Invariant(depth >= 0);
#if !WINDOWS_PHONE_70
			Contract.Invariant(name == null || !string.IsNullOrWhiteSpace(name));
#else
			Contract.Invariant(name == null || !string.IsNullOrEmpty(name));
#endif
			Contract.Invariant(labs != null);
			Contract.Invariant(labs.IsReadOnly);
			Contract.Invariant(subcategories != null);
			Contract.Invariant(subcategories.IsReadOnly);
			////Contract.Invariant(Contract.ForAll(labs, lab => lab != null));
			////Contract.Invariant(Contract.ForAll(subcategories, category => category != Empty));
		}

		public static LabCategory Organize(ICollection<Lab> labs)
		{
			Contract.Requires(labs != null);
			////Contract.Requires(Contract.ForAll(labs, lab => lab != null));
			Contract.Ensures(Contract.Result<LabCategory>() != null);
			Contract.Ensures(Contract.Result<LabCategory>().IsRoot);

			var children = OrganizeRecursive(labs, 0);

			return new LabCategory(children.Item1, children.Item2);
		}

		private static Tuple<List<Lab>, IEnumerable<LabCategory>> OrganizeRecursive(IEnumerable<Lab> labs, int depth)
		{
			Contract.Requires(labs != null);
			////Contract.Requires(Contract.ForAll(labs, lab => lab != null));
			Contract.Requires(depth >= 0);
			Contract.Ensures(Contract.Result<Tuple<List<Lab>, IEnumerable<LabCategory>>>() != null);
			Contract.Ensures(Contract.Result<Tuple<List<Lab>, IEnumerable<LabCategory>>>().Item1 != null);
			Contract.Ensures(Contract.Result<Tuple<List<Lab>, IEnumerable<LabCategory>>>().Item2 != null);
			/*
			Contract.Ensures(Contract.ForAll(Contract.Result<Tuple<List<Lab>, IEnumerable<LabCategory>>>().Item1, lab => lab != null));
			Contract.Ensures(Contract.ForAll(Contract.Result<Tuple<List<Lab>, IEnumerable<LabCategory>>>().Item2, category => category != Empty));
			 */

			var childDepth = depth + 1;

			var labsAtDepth =
				(from lab in labs
				 where lab.Categories.Count <= depth
				 select lab)
				 .ToList();

			var categoriesAtDepth =
				(from lab in labs
				 from category in lab.Categories.Skip(depth).Take(1)
				 select new { Category = category, Lab = lab })
				 .ToList();

			if (labsAtDepth.Count == 0 && (categoriesAtDepth
																		 .Select(lab => lab.Category)
																		 .Distinct(StringComparer.Ordinal)
																		 .Count() <= 1))
			{
				return OrganizeRecursive(labs, childDepth);
			}
			else
			{
				var categories =
					from category in categoriesAtDepth.ToLookup(lab => lab.Category, lab => lab.Lab)
					let children = OrganizeRecursive(category, childDepth)
					select new LabCategory(category.Key, childDepth, children.Item1, children.Item2);

#if !WINDOWS_PHONE
				return Tuple.Create(labsAtDepth, categories);
#else
				var result = Tuple.Create(labsAtDepth, categories);

				Contract.Assume(result.Item1 != null);
				Contract.Assume(result.Item2 != null);

				return result;
#endif
			}
		}

		private IEnumerable<LabCategory> CategoriesRecursiveIterator()
		{
			yield return this;

			foreach (var sub in subcategories)
			{
				foreach (var category in sub.CategoriesRecursive)
				{
					yield return category;
				}
			}
		}

		public override string ToString()
		{
			return name ?? string.Empty;
		}
		#endregion
	}
}